/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package knn;

import java.util.Set;
import Javabeans.DataPoint;
import csvloader.CSVLoader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Random;
import static java.lang.System.out;

/**
 *
 * @author Muhammed
 */
public class KNN {
    private static CSVLoader csvloader;
    private ArrayList<DataPoint> trainingSet, testingSet;
    private int truePredictions, falsePredictions;

    
    public ArrayList<DataPoint> getTrainingSet() {
        return trainingSet;
    }
    public ArrayList<DataPoint> getTestingSet() {
        return testingSet;
    }
    public void loadTrainingData(String path) throws IOException {
        System.out.println("##############Training Set##############");
        trainingSet = loadDataSet(path);
    }
    public void loadTestingData(String path) throws IOException {
        System.out.println("##############Testing Set##############");
        testingSet = loadDataSet(path);
    }
    private ArrayList<DataPoint> loadDataSet(String path) throws IOException {
        csvloader = new CSVLoader(path);
        ArrayList<DataPoint> dataSet = csvloader.getDataSet();
        return dataSet;
    }
    private ArrayList<DataPoint> normalizeDataSet(ArrayList<DataPoint> dataSet) {
        int numOfAttributes = DataPoint.getAttributeNames().length;
        ArrayList[] columns = new ArrayList[numOfAttributes];

        Iterator it = dataSet.iterator();
        while (it.hasNext()) {
            DataPoint customer = (DataPoint) it.next();
            Double[] d = customer.getAttributeValues();
            for (int i = 0; i < d.length; i++) {
                if (columns[i] != null) {
                    columns[i].add(d[i]);
                } else {
                    columns[i] = new ArrayList<Double>();
                }
            }
        }

        for (int i = 0; i < columns.length; i++) {
            Double maxCus = (Double) Collections.max(columns[i]);
            Double minCus = (Double) Collections.min(columns[i]);
            DataPoint.setAttributeStats(new Integer(i),
                    new Double[]{maxCus, minCus});
        }
        return null;
    }
    public void calculateSimilarity(DataPoint newCustomer, ArrayList<DataPoint> trainingSet) {
        Iterator<DataPoint> it = trainingSet.iterator();

        DataPoint.setTestPoint(newCustomer);

        while (it.hasNext()) {
            DataPoint oldCustomer = it.next();
            oldCustomer.calculateDistanceFromTestPoint();
        }
    }
    public DataPoint[] getNeighbours(int numberOfNeighbours, DataPoint newCustomer) {
        //System.out.print("NN for row ["+newCustomer+"]: ");

        DataPoint.setTestPoint(newCustomer);

        Iterator<DataPoint> it = trainingSet.iterator();
        ArrayList<DataPoint> neighbours = new ArrayList<DataPoint>();
        while (it.hasNext()) {
            DataPoint customer = it.next();
            customer.calculateDistanceFromTestPoint();
            neighbours.add(customer);
        }

        Collections.sort(neighbours);


        return neighbours.subList(0, numberOfNeighbours).toArray(new DataPoint[0]);
    }
    public DataPoint[] predictTestingSet(int numberOfNeighbours, boolean printFlag) {
        normalizeDataSet(this.trainingSet);
        if(printFlag){
            out.println("\nNumber of neighbours: " + numberOfNeighbours);
        }
        for (int i = 0; i < getTestingSet().size(); i++) {
            DataPoint testPoint  = getTestingSet().get(i);
            if(testPoint != null){
            DataPoint[] neighbours = getNeighbours(numberOfNeighbours, testPoint);

            //Print all the neighbours
            if(printFlag){
                for (DataPoint c : neighbours) {
                    out.print(c.getOutputValue() + ",");
                }
            }

            HashMap<String, Integer> outputMap = new HashMap<String, Integer>();
            //Predict output based on neighbours
            Integer temp;
            for (DataPoint dp : neighbours) {
                temp = null;
                if ((temp = outputMap.get(dp.getOutputValue())) != null) {
                    outputMap.get(dp.getOutputValue());
                    outputMap.remove(dp.getOutputValue());
                    outputMap.put(dp.getOutputValue(), temp + 1);
                } else {
                    outputMap.put(dp.getOutputValue(), 1);
                }
            }
            DataPoint p = DataPoint.getTestPoint();
            Integer max = Collections.max(outputMap.values());
            String actualOutput = p.getOutputValue();
            for (String output : outputMap.keySet()) {
                if (outputMap.get(output).equals(max)) {
                    p.setOutputValue(output);
                    break;
                }
            }
            if(printFlag){
                out.print("|"+actualOutput+"(a)|"+p.getOutputValue()+"(p)| "+"\n");
            }
            }
        }
        return null;
    }
    public void crossValidate(int folds, int numOfNeighbours, boolean printFlag) {
        this.testingSet = new ArrayList<DataPoint>();
        ArrayList<DataPoint> originalTrainingSet = (ArrayList<DataPoint>) this.trainingSet.clone();
        DataPoint[] originalTestingSet;
        ArrayList<HashMap> confusionMatrixList = new ArrayList<HashMap>();

        DataPoint[][] tempFolds = getFolds(folds);
        out.println("Number of Folds: " + tempFolds.length);
        out.println("Number of Neighbours: " + numOfNeighbours);
        int count = 1;
        for (DataPoint[] fold : tempFolds) {
            /*
            out.println("Doing: Fold#"+count+++" with "+fold.length+" records.");
            for(DataPoint d:fold){
                out.println(d);
            }
            */
            for (DataPoint d : fold) {
                this.testingSet.add(d);
                this.trainingSet.remove(d);
            }
            
            ArrayList<DataPoint> originalTestingList = new ArrayList<DataPoint>();
            Iterator it = testingSet.iterator();
            while(it.hasNext()){
                DataPoint dp = (DataPoint) it.next();
                if(dp!=null){
                    originalTestingList.add((DataPoint) dp.clone());
                }else{
                    originalTestingList.add(null);
                }
            }
            originalTestingSet = originalTestingList.toArray(new DataPoint[0]);


            //Predict outputs for all the datapoints in the testing set
            this.predictTestingSet(numOfNeighbours, printFlag);
/*
            out.println("After: ");
            for(DataPoint p : originalTestingList)
                out.print(p.getOutputValue());
            out.println("");
 * 
 */

            //Generate a confusion matrix
            HashMap temp = generateConfusionMatrix(originalTestingSet);
            confusionMatrixList.add(temp);
            this.trainingSet = originalTrainingSet;
            this.testingSet = new ArrayList<DataPoint>();
        }
        System.out.println("True Predictions: "+truePredictions);
        System.out.println("False Predictions: "+falsePredictions);
        double d = (double)truePredictions/(falsePredictions+truePredictions)*100;
        System.out.println("Precision: "+d);

    }
    private DataPoint[][] getFolds(int numberOfFolds) {
        int foldSize = this.trainingSet.size() / numberOfFolds;
        int mod = this.trainingSet.size() % numberOfFolds;
        if (mod != 0) {
            foldSize++;
        }
        DataPoint[][] folds = new DataPoint[numberOfFolds][foldSize];

        ArrayList<DataPoint> tempTrainingSet = (ArrayList<DataPoint>) this.trainingSet.clone();


        Random randomGenerator;
        int random;
        for (int i = 0; i < foldSize; i++) {
            for (int j = 0; j < numberOfFolds && tempTrainingSet.size() != 0; j++) {
                randomGenerator = new Random();
                random = randomGenerator.nextInt(tempTrainingSet.size());
                folds[j][i] = tempTrainingSet.get(random);
                tempTrainingSet.remove(random);
            }
        }
        /*
        for (int i = 0; i < numberOfFolds; i++) {
            for (int j = 0; j < foldSize; j++) {
                System.out.print(folds[i][j] + "\t ");
            }

            System.out.println();
        }
         System.out.println("Done");
         * 
         */
        return folds;
    }
    private HashMap<String, HashMap<String, Integer>> generateConfusionMatrix(DataPoint[] originalTestingSet) {
        HashMap<String, HashMap<String, Integer>> confusionMatrix =
                new HashMap<String, HashMap<String, Integer>>();
        HashMap<String, Integer> tempMap;
        //check if original and predicted are the same size
        if(originalTestingSet.length==this.testingSet.size()){
            //compare pre and ori to populate confusion matrix
            for(int i =0;i<originalTestingSet.length;i++){
                if(originalTestingSet[i]!=null&&this.testingSet.get(i)!=null){
                    String actual = originalTestingSet[i].getOutputValue();
                    String predicted = this.testingSet.get(i).getOutputValue();
                    if(actual.compareTo(predicted)==0){
                        truePredictions++;
                    }else{
                        falsePredictions++;
                    }
                    if((tempMap=confusionMatrix.get(actual))==null){
                        tempMap = new HashMap<String, Integer>();
                        tempMap.put(predicted, 1);
                        confusionMatrix.put(actual, tempMap);
                    }else{
                       Integer count = tempMap.get(predicted);
                       if(count!=null){
                           count+=1;
                           tempMap.remove(predicted);
                           tempMap.put(predicted, count);
                       }else{
                           tempMap.put(predicted, 1);
                       }
                    }
                }
            }
            return confusionMatrix;
        }
        return null;
    }
    public double getPrecision(){
        return (truePredictions/(falsePredictions+truePredictions))*100;
    }
}
